<?php
/**
 */
class Yz_Action_Select
    extends Yz_Action_Abstract
{

    const FJOIN = '_';

    /**
     *  @var array( fieldName: fieldSelectClause )
     */
    protected $fields = array();

    /** SQL rows limit, set to 1 for base Select to avoid occasionally selecting all rows
     *  @var int(1)
     */
    protected $limit = 1;

    protected $fieldStack = array();
    protected $substAlias = array();
    protected $fieldStackString;
    protected $result = array();
    protected $resultPtr;

    /************************************
     *          Public methods          *
     ************************************/


    /**
     *  @return Yz_Hash
     */
    public function fetch()
    {
        if (! $this->executed)
            $this->perform();
            // throw StateError::_new_();
        $values = $this->statement->fetch();
        if (! $values)
            throw NotFoundError::_new_();
        return $this->bindResult($values);
    }



    /************************************
     *        Protected methods         *
     ************************************/

    /**
     *  Apply the arguments to the SQL statement
     *      Setup limit to avoid occasional selection of all rows
     *  @param Yz_SQL_Statement $sql
     *  @return this
     */
    protected function applyArguments(Yz_SQL_Statement $sql)
    {
        debug();
        if ($this->limit)
            $sql->limit($this->limit);
        return $this;
    }

    /**
     *  Common SELECT statement creator:
     *      the common part of statement
     *  @return Yz_SQL_Statement_Select
     */
    protected function buildStatement()
    {
        debug();
        //$this->result = new Yz_Hash();
        $this->inspectModel();
        return Yz_SQL_Statement_Select::create()
            ->select($this->fields)
            ->from($this->tables)
        ;
    }



    /**
     *  Inspect model's property and create a field clause or
     *      recursively inspect referenced model
     *  @param Yz_Model_Interface $model
     *  @param string $alias
     *  @param string $propName
     *  @param Yz_Property_Interface $prop
     *  @return this
     */
    protected function inspectField(Yz_Model_Interface $model, $alias, $propName, Yz_Property_Interface $prop)
    {
        debug("Field: {$alias}.{$propName}");

        $stack = $this->fieldStack ? join('.', $this->fieldStack) . '.' : "";
        $f = $stack . $propName;

        $parent = $model->getParentModel();
        if ($parent and $parent->offsetExists($propName)) {
            debug("Skipping property {$propName} which exists in parent {$parent}");
            // do nothing
        } else if ($prop instanceof Yz_Property_Interface_Reference) {
            $this->result[$f] = $prop;
            $this->inspectReferenceField($model, $alias, $propName, $prop);
        } else {

            // TODO check custom settings for the field
            $clause = "{$alias}.{$propName}";

            if ($clause) {
                #$stack = $this->fieldStack; // ?: array($alias);
                $this->fields[$f] = $clause;
                $this->result[$f] = $prop;
            }
        }
        return $this;
    }

    /**
     *
     */
    protected function inspectReferenceField(
            Yz_Model_Interface $origModel,      // original model (to which the field belongs)
            $alias,
            $propName,
            Yz_Property_Interface $prop)
    {

        debug("Ref Field: {$alias}.{$propName}");
        $refModel = $prop->getModel();

        debug("Ref Model: {$refModel}");

        // in result, get the value MyModel_RefProp_property and
        // put it in out.MyModel.RefProp.property as
        $stack = $this->fieldStack ? join('.', $this->fieldStack) . '.' : "";
        $f = $stack . $propName;

        if ($origModel === $refModel) {
            // reference to self
            $this->fields[$f] = "{$alias}.{$propName}Id"; // e.g. MyModel.parentId AS MyModel_parent
            // do not create result item, use id as value
        } else {
            $refAlias = $this->enterProperty($propName);

            if ($prop instanceof Yz_Property_OneToOne) {
                // models are joined by id
                // add two field clauses: for "my" id and for "parent's" id
                // which have the same values
                // e.g. MyModel.id AS MyModel__UpModel, _UpModel.id AS _UpModel_id
                $this->fields[$f] = "{$alias}.id";
                $propName = 'id'; // join on id
            } else {
                // the property is a foreign key
                // do not add MyModel.referenceId since MyModel_Reference_id is same
                $propName .= 'Id';
            }
            $this
                ->addSource($origModel, $alias, $propName, $refModel, $refAlias)
                ->doInspectModel($refModel, $refAlias)
            ;
            $this->exitProperty();
        }

        return $this;
    }



    /** Add a table to table list using info from property
     *  @param $origModel, $origField, $refModel
     *  NOTE: the resulting origField will have 'Id' added (eg. type -> typeId)
     */
    protected function addSource($origModel, $origAlias, $origField, $refModel, $refAlias)
    {
        debug("Add {$refModel} as {$refAlias}");
        $key       = 'id';                          // use referenced model's primary key field name
        $refTable  = $refModel->getSourceName();       // ref.model table name
        // TODO
        //  if prop.allowNull : use LEFT JOIN
        //  otherwise, use INNER JOIN
        $table = Yz_SQL_Join::leftJoin($refTable, $refAlias)
            ->joinOn("{$refAlias}.{$key}={$origAlias}.{$origField}")
        ;
        $this->tables[$refAlias] = $table;

        return $this;
    }


    /** Enter a property into stack
     *
     */
    protected function enterProperty($propName)
    {
        $this->fieldStack[] = $propName;
        return join(self::FJOIN, $this->fieldStack);
    }

    /** Pop last entered property off stack
     *
     */
    protected function exitProperty()
    {
        array_pop($this->fieldStack);
        return $this;
    }

    /** Pop last entered property off stack
     *
     */
    protected function bindResult($values)
    {
        debug();
        $out = array();
        foreach ($this->result as $f => $prop) {
            #debug("Field: {$f}");
            #debug("Value: {$values[$f]}");
            if ($prop instanceof Yz_Property_SelfReference)
                $v = array('id' => $values[$f]);
            else if ($prop instanceof Yz_Property_Interface_Reference)
                $v = array(); // its id will be assigned in one of next iterations
            else
                $v = $values[$f];
            $r = str_replace('.', "']['", $f);
            eval("\$out['{$r}'] = \$v;");
        }
        return $out;
    }


}
